/*
 * Copyright (C) 2008,2009,2010,2011  Paulo Cesar Pereira de Andrade.
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * Authors:
 *	Paulo Cesar Pereira de Andrade
 */

#include "exl.h"

#include <fcntl.h>
#include <sys/stat.h>

/*
 * Prototypes
 */
static eint32_t
read_buffer(estream_t *stream);

static void
builtin_open(eint32_t ac);

static void
builtin_seek(eint32_t ac);

static void
builtin_close(eint32_t ac);

static ssize_t
sys_read(int fd, void *data, size_t count);

static ssize_t
sys_write(int fd, void *data, size_t count);

static off_t
sys_seek(int fd, off_t offset, int whence);

static void
stream_w_flush(estream_t *stream);

/*
 * Initialization
 */
estream_t *std_input;
estream_t *std_output;
estream_t *std_error;

/*
 * Implementation
 */
void
init_stream(void)
{
    esymbol_t	*symbol;

    efdopen((eobject_t *)&std_input, 0, S_read | S_buffered | S_source);
    eadd_root((eobject_t *)&std_input);
    std_input->name = eget_string((euint8_t *)"<stdin>", 7);

    efdopen((eobject_t *)&std_output, 1, S_write | S_buffered);
    eadd_root((eobject_t *)&std_output);
    std_output->name = eget_string((euint8_t *)"<stdout>", 8);

    efdopen((eobject_t *)&std_error, 2, S_write | S_unbuffered);
    eadd_root((eobject_t *)&std_error);
    std_error->name = eget_string((euint8_t *)"<stderr>", 8);

    symbol = enew_symbol(ht_language, eget_string((euint8_t *)"open", 4));
    edefine_builtin(enew_bound_symbol(symbol), builtin_open, 2, false);

    symbol = enew_symbol(ht_language, eget_string((euint8_t *)"seek", 4));
    edefine_builtin(enew_bound_symbol(symbol), builtin_seek, 3, false);

    symbol = enew_symbol(ht_language, eget_string((euint8_t *)"close", 5));
    edefine_builtin(enew_bound_symbol(symbol), builtin_close, 1, false);
}

void
efdopen(eobject_t *pointer, int fileno, eint32_t mode)
{
    estream_t	*stream;
    struct stat	 st;

    enew(pointer, stream);
    stream = *(estream_t **)pointer;

    stream->s_read = (mode & S_read) != 0;
    stream->s_write = (mode & S_write) != 0;
    stream->s_append = (mode & S_append) != 0;
    if ((stream->s_source = (mode & S_source) != 0 &&
	 (mode & (S_read|S_write)) == S_read))
	stream->line = 1;

    if (fstat(fileno, &st) == 0)
	stream->s_regular = S_ISREG(st.st_mode) != 0;

    /* if not explicitly set to unbuffered */
    if (!(mode & S_unbuffered) &&
	/* if regular file or explicitly set to buffered */
	(stream->s_regular || (mode & S_buffered)))
	stream->s_buffered = 1;

    /* always allocate a buffer */
    v_check(1);
    v_push(stream);
    enew_object((eobject_t *)&stream->ptr, t_resize | t_uint8, BUFSIZ);
    v_dec();

    stream->fileno = fileno;

    emutex_init(&stream->mutex);
}

void
efopen(eobject_t *pointer, evector_t *path, eint32_t mode)
{
    int		 flags;
    int		 fileno;
    estream_t	*stream;
    eint32_t	 length;
    char	 name[BUFSIZ];

    flags = O_NOCTTY;

    v_check(1);
    v_push(path);

    /* check read/write attributes */
    if ((mode & S_read) && (mode & S_write))
	flags |= O_RDWR;
    else if (mode & S_read)
	flags |= O_RDONLY;
    else if (mode & S_write)
	flags |= O_WRONLY;

    if (mode & S_nonblock)
	flags |= O_NONBLOCK;

    /* create if does not exist */
    if (mode & S_write) {
	flags |= O_CREAT;

	/* append if exists */
	if (mode & S_append)
	    flags |= O_APPEND;
	/* assume read/write mode doesn't want to truncate file */
	else if (!(mode & S_read))
	    flags |= O_TRUNC;
    }

    if ((length = path->length) >= sizeof(name) - 1) {
	ewarn("pathname too long");
	length = sizeof(name) - 1;
    }
    memcpy(name, path->v.obj, length);
    name[length] = '\0';

    if ((fileno = open(name, flags, 0666)) >= 0) {
	efdopen(pointer, fileno, mode);
	if ((stream = *(estream_t **)pointer))
	    stream->name = path;
    }
    else
	*pointer = null;
    v_dec();
}

off_t
eseek(estream_t *stream, off_t position, eint32_t whence)
{
    off_t	offset;

    offset = 0;
    if (eobject_type(stream) == t_stream) {
	if (stream->s_regular) {
	    if (position == 0 && whence == SEEK_CUR) {
		offset = sys_seek(stream->fileno, 0, SEEK_CUR);
		offset += stream->offset;
	    }
	    else {
		if (stream->s_r_mode) {
		    stream->offset = stream->length = 0;
		    stream->s_r_mode = 0;
		}
		else if (stream->s_w_mode)
		    stream_w_flush(stream);
		offset = sys_seek(stream->fileno, position, whence);
	    }
	}
    }
    else {
	switch (whence) {
	    case SEEK_CUR:
		position += stream->offset;
		break;
	    case SEEK_END:
		position += stream->length;
		break;
	    default:
		/* FIXME check if not SEEK_SET,
		 * or lseek "compatible" errors */
		break;
	}
	if (position < 0)			position = 0;
	else if (position > stream->length)	position = stream->length;
	stream->offset = position;
	offset = position;
    }

    return (offset);
}

ssize_t
eread(estream_t *stream, void *data, size_t count)
{
    size_t	left;
    ssize_t	bytes;

    bytes = eof;
    if (eobject_type(stream) == t_stream) {
	if (!stream->s_read)
	    goto done;
	else if (stream->s_append) {
	    bytes = 0;
	    goto done;
	}

	/* switching operation */
	if (stream->s_w_mode)
	    stream_w_flush(stream);

	/* bytes left to read */
	left = count;

	/* if there is anything in the buffer */
	if (stream->s_r_mode) {
	    bytes = stream->length - stream->offset;
	    if (bytes > left)
		bytes = left;
	    memcpy(data, stream->ptr + stream->offset, bytes);
	    /* if buffer became empty */
	    if ((stream->offset += bytes) == stream->length) {
		stream->offset = stream->length = 0;
		stream->s_r_mode = 0;
	    }
	    left -= bytes;
	}

	/* if still needs to read data */
	if (left) {
	    bytes = left - (left % BUFSIZ);

	    /* don't cache or iterate over large chunks */
	    if (bytes) {
		bytes = sys_read(stream->fileno,
				 (eint8_t *)data + (count - left), bytes);
		left -= bytes;
	    }

	    /* chunk is smaller than, or not aligned to BUFSIZ bytes */	
	    if (left) {
		/* read any padding data to buffer */
		bytes = sys_read(stream->fileno, stream->ptr,
				 eobject_length(stream->ptr));
		stream->offset = 0;
		stream->length = bytes;
		stream->s_r_mode = 1;

		/* read remaining data from buffer */
		if (bytes > left)
		    bytes = left;
		memcpy((eint8_t *)data + (count - left), stream->ptr, bytes);
		stream->offset += bytes;
		left -= bytes;
	    }
	}
	bytes = count - left;
    }
    else {
	/* number of bytes available in current buffer */
	bytes = stream->length - stream->offset;
	/* if there are excess bytes in current buffer */
	if (bytes > count)
	    bytes = count;
	/* read data */
	memcpy(data, stream->ptr + stream->offset, bytes);
	/* update stream offset */
	stream->offset += bytes;
    }

done:
    return (bytes);
}

ssize_t
ewrite(estream_t *stream, void *data, size_t count)
{
    ssize_t	left;
    ssize_t	bytes;
    eint32_t	length;

    bytes = eof;
    if (eobject_type(stream) == t_stream) {
	if (!stream->s_write)
	    goto done;

	/* bytes left to write */
	left = count;

	if (stream->s_r_mode) {
	    assert(stream->s_r_lock == false);
	    /* switching operation, discard buffer contents */
	    sys_seek(stream->fileno, stream->offset - stream->length, SEEK_CUR);
	    stream->offset = stream->length = 0;
	    stream->s_r_mode = 0;
	}
	/* if there is anything in the buffer */
	else if (stream->s_w_mode) {
	    length = eobject_length(stream->ptr);
	    bytes = length - stream->offset;
	    if (bytes > left)
		bytes = left;
	    memcpy(stream->ptr + stream->offset, data, bytes);
	    if ((stream->offset += bytes) > stream->length)
		stream->length = stream->offset;

	    /* if write buffer is full */
	    if (stream->length >= length)
		stream_w_flush(stream);
	    left -= bytes;
	}

	/* if still need to write data */
	if (left) {
	    bytes = left - (left % BUFSIZ);

	    /* don't cache or iterate over large chunks */
	    if (bytes) {
		bytes = sys_write(stream->fileno,
				  (eint8_t *)data + (count - left), bytes);
		left -= bytes;
	    }

	    if (left) {
		/* write any padding data to buffer */
		memcpy(stream->ptr, (eint8_t *)data + (count - left), left);
		if ((stream->offset += left) > stream->length)
		    stream->length = stream->offset;
		/* only reach here if flushed buffer */
		stream->s_w_mode = 1;
	    }
	}
	bytes = count;
    }
    else {
	/* check if need more space */
	if (stream->offset + count > eobject_length(stream->ptr)) {
	    bytes = eobject_length(stream->ptr) + count;
	    bytes += BUFSIZ - (bytes % BUFSIZ);
	    eresize_object((eobject_t *)&stream->ptr, bytes);
	}
	memcpy(stream->ptr + stream->offset, data, count);
	if ((stream->offset += count) > stream->length)
	    stream->length = stream->offset;
	bytes = count;
    }

done:
    return (bytes);
}

ebool_t
eflush(estream_t *stream)
{
    if (eobject_type(stream) == t_stream) {
	if (stream->s_write && stream->s_w_mode) {
	    stream_w_flush(stream);

	    return (true);
	}
    }

    return (false);
}

eint32_t
egetc(estream_t *stream)
{
    eint32_t	 ch;

    ch = eof;
    if (eobject_type(stream) == t_stream) {
	if (stream->s_read) {
	    if (stream->s_w_mode)
		stream_w_flush(stream);
	    if (stream->offset < stream->length || read_buffer(stream)) {
		ch = stream->ptr[stream->offset++];
		if (stream->s_source && ch == '\n')
		    ++stream->line;
	    }
	}
    }
    else if (stream->offset < stream->length)
	ch = stream->ptr[stream->offset++];

    return (ch);
}

void
eungetc(estream_t *stream, eint32_t ch)
{
    ebool_t	stream_p;

    if (ch == eof)
	return;

    stream_p = eobject_type(stream) == t_stream;

    if (stream_p) {
	if (!stream->s_read)
	    return;

	if (stream->s_w_mode)
	    stream_w_flush(stream);
	else if (stream->s_source && ch == '\n')
	    --stream->line;

	if (stream->offset)
	    --stream->offset;
	else if (stream->length) {
	    if (stream->length >= eobject_length(stream->ptr)) {
		eresize_object((eobject_t *)&stream->ptr,
			       stream->length + BUFSIZ);
		memmove(stream->ptr + 1, stream->ptr, stream->length);
		*stream->ptr = ch;
		++stream->length;
	    }
	    else {
		*stream->ptr = ch;
		stream->length = 1;
		if (stream_p)
		    stream->s_r_mode = 1;
	    }
	}
    }
    else if (stream->offset)
	--stream->offset;
}

eint32_t
eputc(estream_t *stream, eint32_t byte)
{
    eint32_t	length;

    if (eobject_type(stream) == t_stream) {
	if (stream->s_write) {
	    if (stream->s_r_mode) {
		assert(stream->s_r_lock == false);
		/* switching operation, discard buffer contents */
		sys_seek(stream->fileno, stream->offset - stream->length,
			 SEEK_CUR);
		stream->offset = stream->length = 0;
		stream->s_r_mode = 0;
	    }
	    else if (stream->length >= eobject_length(stream->ptr))
		stream_w_flush(stream);
	    stream->ptr[stream->offset] = byte;
	    if (++stream->offset > stream->length)
		stream->length = stream->offset;
	    stream->s_w_mode = 1;
	}
	else
	    byte = eof;
    }
    else {
	length = eobject_length(stream->ptr);
	if (stream->offset >= length)
	    eresize_object((eobject_t *)&stream->ptr, length + BUFSIZ);
	stream->ptr[stream->offset] = byte;
	if (++stream->offset > stream->length)
	    stream->length = stream->offset;
    }

    return (byte);
}

eint32_t
eclose(estream_t *stream)
{
    eint32_t	result;

    if (stream->fileno != -1) {
	if (stream->s_w_mode)
	    stream_w_flush(stream);
	result = close(stream->fileno);
	stream->fileno = -1;
	stream->s_read = stream->s_write = 0;
	emutex_destroy(&stream->mutex);
    }
    else
	result = -1;

    return (result);
}

void
echeck_buffer(estream_t *stream, eint32_t bytes)
{
    eint32_t	length;
    ebool_t	stream_p;

    stream_p = eobject_type(stream) == t_stream;

    length = eobject_length(stream->ptr);
    if (stream->offset + bytes > length) {
	if (stream_p && stream->s_w_mode) {
	    stream_w_flush(stream);
	    if (bytes <= length)
		return;
	}
	length = length + bytes;
	length += BUFSIZ - length % BUFSIZ;
	eresize_object((eobject_t *)&stream->ptr, length);
    }
}

void
eread_lock(estream_t *stream)
{
    eint32_t	bytes;

    if (eobject_type(stream) == t_stream) {
	assert(stream->s_r_lock == false);
	stream->s_r_lock = 1;

	/* avoid possibly cyclic read_lock() call just when buffer is filled */
	if (stream->offset >= BUFSIZ) {
	    bytes = stream->length - stream->offset;
	    memmove(stream->ptr, stream->ptr + stream->offset, bytes);
	    stream->offset = 0;
	    stream->length = bytes;
	}
    }
}

void
eread_unlock(estream_t *stream)
{
    if (eobject_type(stream) == t_stream) {
	assert(stream->s_r_lock != false);
	stream->s_r_lock = 0;
    }
}

static eint32_t
read_buffer(estream_t *stream)
{
    eint32_t	bytes;
    eint32_t	count;
    eint32_t	length;
    ebool_t	stream_p;

    stream_p = eobject_type(stream) == t_stream;

    /* if there is anything to read */
    if ((count = stream->length - stream->offset) > 0)
	goto done;

    /* if reading from a string stream */
    if (!stream_p) {
	count = 0;
	goto done;
    }

    /* dont block, only read more data when buffer is empty */
    length = eobject_length(stream->ptr);
    bytes = length - stream->length;
    if (bytes <= 0) {
	if (stream->s_r_lock) {
	    count = length + BUFSIZ;
	    count = count - (count % BUFSIZ);
	    eresize_object((eobject_t *)&stream->ptr, count);
	    bytes = sys_read(stream->fileno, stream->ptr + stream->offset,
			     count - length);
	    if (bytes > 0) {
		stream->length += bytes;
		count = bytes;
	    }
	    else
		count = 0;
	}
	else {
	    stream->offset = 0;
	    bytes = sys_read(stream->fileno, stream->ptr, length);
	    if (bytes > 0) {
		stream->length = count = bytes;
		stream->s_r_mode = 1;
	    }
	    else
		stream->s_r_mode = 0;
	}
    }
    else {
	bytes = sys_read(stream->fileno, stream->ptr + stream->length, bytes);
	if (bytes > 0) {
	    count = bytes;
	    stream->s_r_mode = 1;
	    stream->length += bytes;
	}
    }

done:
    return (count);
}

static void
builtin_open(eint32_t ac)
{
    evalue_t	*v;
    evector_t	*name;

    v = thread_self->esp - 1;
    name = v->v.o;
    switch (eobject_type(name) & ~t_const) {
	case t_string:
	    --v;
	    if (v->t == t_int) {
		efopen(&thread_self->obj, name, v->v.i);
		thread_self->ret.v.o = thread_self->obj;
		break;
	    }
	default:
	    thread_self->ret.v.o = null;
    }
    thread_self->ret.t = eobject_type(thread_self->ret.v.o);
}

static void
builtin_seek(eint32_t ac)
{
    evalue_t	*v;
    estream_t	*stream;
    off_t	 offset;

    v = thread_self->esp - 1;
    stream = v->v.o;
    offset = -1;
    switch (eobject_type(stream) & ~t_const) {
	case t_stream:
	case t_string:
	    --v;
	    if (v->t == t_int) {
		offset = v->v.i;
		--v;
		if (v->t == t_int)
		    offset = eseek(stream, offset, v->v.i);
		else
		    offset = -1;
	    }
	default:
	    break;
    }

    thread_self->ret.v.i = offset;
    thread_self->ret.t = t_int;
}

static void
builtin_close(eint32_t ac)
{
    evalue_t	*v;

    v = thread_self->esp - 1;
    thread_self->ret.v.i = eobject_type(v->v.o) == t_stream &&
	eclose(v->v.o) == 0 ? true : false;
    thread_self->ret.t = t_int;
}

static ssize_t
sys_read(int fd, void *data, size_t count)
{
    ssize_t bytes;

    if ((bytes = read(fd, data, count)) < 0)
	ewarn("I/O error");

    return (bytes);
}

static ssize_t
sys_write(int fd, void *data, size_t count)
{
    ssize_t bytes;

    if ((bytes = write(fd, data, count)) != count)
	ewarn("I/O error");

    return (bytes);
}

static off_t
sys_seek(int fd, off_t offset, int whence)
{
    off_t	position;

    if ((position = lseek(fd, offset, whence)) < 0)
	ewarn("I/O error");

    return (position);
}

static void
stream_w_flush(estream_t *stream)
{
    sys_write(stream->fileno, stream->ptr, stream->length);
    stream->length = stream->offset = 0;
    stream->s_w_mode = 0;
}
